package core;

import java.io.*;
import java.net.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import libs.Flag;
import libs.Payload;

/**
 * @author Ciaran Liedeman
 * @author Dirk Terblanche
 * @version 0.4
 *
 */
public class Server {

    private DatagramSocket serverSocket = null;     // Server packet socket
    private int port;                               // server listening port
    private ServerThread serverThread;              // TODO: make server thread singular
    private ConcurrentHashMap clients;                            // List of connected clients
    //TODO make client hash map for storing currently connected clients - concurrent?
    private static int BYTE_ARRAY_LENGTH = 5000;

    /**
     * Default Constructor. Assumes port 3000 and localhost
     */
    public Server() {
        this(3000);
    }

    /**
     * Server Constructor, Starts Server Thread and calls initialize
     * @param port - listening port for server socket
     * @param iaddress - ip address to bind port to
     */
    public Server(int port) {
        this.port = port;

        System.out.println("Server Started");

        try {
            // Attempt to create socket on port
            serverSocket = new DatagramSocket(port);
        } catch (IOException ex) {
            System.err.println("Error: Cannot Listen on port: " + port);
            System.exit(1);
        }

        System.out.println("Sever Socket opened on port " + port);

        serverThread = new ServerThread();
        serverThread.start();
    }

    private void clientDisconnect(String username) {
        clients.remove(username);
    }

    public InetAddress getAddress(String username) {
        InetAddress address = (InetAddress)clients.get(username);
        return address;
    }

    public ConcurrentHashMap getClientList() {
        return clients;
    }

    private boolean addClient(String username, InetAddress address){
        if (clients.containsKey(username)){
            return false;
        }
        else{
            clients.put(username, address);
            return true;
        }
    }

    private void broadcast(Payload payload) {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000);
        ObjectOutputStream outputStream;

        try {
            outputStream = new ObjectOutputStream(new BufferedOutputStream(byteStream));
            outputStream.flush();
            outputStream.writeObject(payload);
            outputStream.flush();
        } catch (IOException ex) {
            System.err.println("Output Stream IO Error");
            System.exit(1);
        }

        byte[] buf = byteStream.toByteArray();
        InetAddress group= null;

        try {
            group = InetAddress.getByName("230.0.0.1");
        } catch (UnknownHostException ex) {
            Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
        }

        DatagramPacket packet = new DatagramPacket(buf, buf.length, group, 3001);

        try {
            serverSocket.send(packet);
        } catch (IOException ex) {
            System.err.println("IO Error");
            return;
        }
    }

    private void directMessage(Payload payload, InetAddress clientAddress) {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000);
        ObjectOutputStream outputStream;

        try {
            outputStream = new ObjectOutputStream(new BufferedOutputStream(byteStream));
            outputStream.flush();
            outputStream.writeObject(payload);
            outputStream.flush();
        } catch (IOException ex) {
            System.err.println("Output Stream IO Error");
            System.exit(1);
        }

        byte[] buf = byteStream.toByteArray();
        DatagramPacket packet = new DatagramPacket(buf, buf.length, clientAddress, 3001);

        try {
            serverSocket.send(packet);
        } catch (IOException ex) {
            System.err.println("IO Error");
            return;
        }
    }

    private class ServerThread extends Thread {

        private volatile boolean listening = true;
        private DatagramPacket packet;

        ServerThread() {
            // TODO Cleanup code
        }

        /**
         * Listen for incoming packets from clients
         */
        @Override
        public void run() {
            while (listening) {
                byte[] buf = new byte[BYTE_ARRAY_LENGTH];
                packet = new DatagramPacket(buf, buf.length);

                try {
                    serverSocket.receive(packet);   // block until packet received
                } catch (IOException ex) {
                    System.err.println("Error: IO Exception");
                    System.exit(1);
                }
                ProcessPacket process = new ProcessPacket(packet);
                process.start();
            }
        }
    }

    private class ProcessPacket extends Thread{
        private DatagramPacket packet;
        private ObjectInputStream inputStream;
        private Payload<String> payload;
        private ByteArrayInputStream byteStream;

        ProcessPacket(DatagramPacket packet){
            this.packet = packet;
        }

         @Override
        public void run() {
            byteStream = new ByteArrayInputStream(packet.getData());
            try {
                inputStream = new ObjectInputStream(new BufferedInputStream(byteStream));
            } catch (IOException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
            }
            InetAddress clientAddress;
            clientAddress = packet.getAddress();
            payload = receivePayload(packet);
            processPayload(payload, clientAddress);
         }

        private Payload receivePayload(DatagramPacket packet) {
            try {
                payload = (Payload<String>) inputStream.readObject();
            } catch (IOException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
            } catch (ClassNotFoundException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
            }

            try {
                inputStream.close();
                byteStream.close();
            } catch (IOException ex) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
            }

            return payload;
        }

        private void processPayload(Payload<String> payload, InetAddress clientAddress) {
            Payload newLoad;
            switch (payload.getFlag()) {
                case CLIENT_ATTEMPT_CONNECT:
                    boolean success = addClient(payload.getSource(), clientAddress);
                    if(success){
           /*             newLoad = new Payload<String>(Flag.CLIENT_CONNECT_SUCEEDED, payload.getData(), payload.getSource(),);
                        broadcast(newLoad);*/
                    }
                    else{
             /*           newLoad = new Payload<String>(Flag.CLIENT_CONNECT_FAILED, payload.getData(), payload.getSource(),);
                        directMessage(newLoad, clientAddress);*/
                    }
                    break;

                case CLIENT_MESSAGE:
                    broadcast(payload);
                    break;

                case CLIENT_WHISPHER:
                    directMessage(payload, clientAddress);
                    break;

                case REQUEST_CLIENT_LIST:
             //       newLoad = new Payload<ConcurrentHashMap>(Flag.SERVER_SENDING_CLIENT_LIST, getClientList(), payload.getClient());
          //          directMessage(newLoad, clientAddress);
                    break;
                case CLIENT_DISCONNECT:
         //           newLoad = new Payload<String>(Flag.CLIENT_DISCONNECT, payload.getData(), payload.getClient());
        //            broadcast(newLoad);
                    break;
            }
        }
    }
}